package org.oep.jmingle;

import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;

import javax.microedition.lcdui.Image;
import org.oep.game.Actor;

/**
 * All the miscellaneous functions that I need to use from time to time.
 * @author pmkilgo
 *
 */
public class Utils {
	
	private static final String consonants = "bcdfghjklmnpqrstvwxyz";
	private static final String vowels = "aeiou";
	private static final String hardConsonants = "cdgkpqt";
	private static final String softConsonants = "flnrs";
	
	/**
	 * Give a time difference down to the millisecond in a string.
	 * @param diff
	 * @return something like "1.100s"
	 */
	public static String preciseTimeDiff(long diff) {
		int ms = (int) (diff % 1000);
		long seconds = diff / 1000;
		
		long minutes = seconds / 60;
		seconds %= 60;
		
		long hours = minutes / 60;
		minutes %= 60;
		
		long days = hours / 24;
		hours %= 24;
		
		long weeks = days / 7;
		days %= 7;
		
		long years = weeks / 52;
		weeks %= 52;
		
		double tmp;
		if(years > 0) {
			// Approximated
			tmp = years + (diff % 3.1556926e10) / (double) 3.1556926e10;
			return roundDouble(tmp) + "y";
		}
		else if(weeks > 0) {
			tmp = weeks + (diff % 604800000) / 604800000.;
			return roundDouble(tmp) + "w";
		}
		else if(days > 0) {
			tmp = days + (diff % 86400000) / 86400000.;
			return roundDouble(tmp) + "d";
		}
		else if(hours > 0) {
			tmp = hours + (diff % 3600000) / 3600000.;
			return roundDouble(tmp) + "h";
		}
		else if(minutes > 0) {
			tmp = minutes + (diff % 60000) / 60000.;
			return roundDouble(tmp) + "m";
		}
		else if(seconds > 0) {
			tmp = seconds + ms / 1000.;
			return roundDouble(tmp) + "s";
		}
		else {
			return ms + "ms";
		}
		
	}
	
	/**
	 * Returns a time difference in a human-friendly format. Heavily rounded.
	 * @param diff
	 * @return something like "12h"
	 */
	public static String friendlyTimeDiff(long diff) {
		int ms = (int) (diff % 1000);
		long seconds = diff / 1000;
		long minutes = seconds / 60;
		long hours = minutes / 60;
		long days = hours / 24;
		long weeks = days / 7;
		
		String out = new String();

		if(weeks > 0){
			out += weeks + "w";
		} else if(days > 0) {
			out += days + "d";
		} else if(hours > 0) {
			out += hours + "h";
		} else if(minutes > 0) {
			out += minutes + "m";
		} else if(seconds > 0){
			out += seconds + "s";
		} else {
			out += ms + "ms";
		}
		return out;
	}
	
	/**
	 * Return the difference between now and the passed time
	 * @param time
	 * @return something like "12h"
	 */
	public static String friendlyTimeDiffNow(long time) {
		if(time < 0) return "Never";
		return friendlyTimeDiff(System.currentTimeMillis() - time);
	}
	
	/**
	 * Convert hexadecimal integers into decimal integers
	 * @param number, the hex number
	 * @return a long representing the decimal equivalent
	 */
	public static long hexToDecimal(String number) {
		long dec = 0;
		for(int i = 0; i < number.length(); i++) {
			char c = number.charAt(i);
			long trueval = hexDigitEquivalent(c) * pow(16, number.length() - i - 1);
			dec += trueval;
		}
		return dec;
	}
	
	/**
	 * Map a given hex char to its equivalent decimal digit
	 * @param c
	 * @return decimal value of hex char, default is 0
	 */
	public static int hexDigitEquivalent(char c) {
		c = java.lang.Character.toUpperCase(c);
		switch(c) {
		case '1': return 1;
		case '2': return 2;
		case '3': return 3;
		case '4': return 4;
		case '5': return 5;
		case '6': return 6;
		case '7': return 7;
		case '8': return 8;
		case '9': return 9;
		case 'A': return 10;
		case 'B': return 11;
		case 'C': return 12;
		case 'D': return 13;
		case 'E': return 14;
		case 'F': return 15;
		default: return 0;
		}
	}
	
	/**
	 * J2ME lacks this math function, so here is an almost-good equivalent.
	 * @param b, the base
	 * @param e, the exponent
	 * @return
	 */
	public static long pow(long b, long e) {
		long n = 1;
		for(int i = 0; i < e; i++)
			n = n * b;
		return n;
	}
	
	/**
	 * Return a made-up name for a given hex address
	 * @param hex
	 * @return something like "Apubiyu"
	 */
	public static String makeName(String hex) {
		hex = sanitizeMac(hex);
		long seed = Utils.hexToDecimal(hex);
		int iseed = Math.abs((int) seed);
		int size = (iseed % 5) + 3;

				
		String name = new String();
		while(name.length() < size) {
			
			// We get our "random" number by extracting two digits out
			// of the passed hex string (a mac address) and converting
			// it to decimal...
			int s = 2 + name.length();
			int rn = (int) Utils.hexToDecimal(hex.substring(s, s + 2)); 
			
			// Half the time we try to add a vowel
			if(rn % 2 == 0) {
				// If this is the beginning, a vowel should be fine.
				if(name.length() == 0) {
					// Add a vowel by default
					name += vowels.charAt( rn % vowels.length() );
				}
				
				// If the previous letter was not a vowel, it should be fine
				else if(vowels.indexOf( name.charAt(name.length() - 1) ) < 0) {
					name += vowels.charAt( rn % vowels.length() );
				}
				
				// Otherwise, a consonant shall suffice
				else {
					// Add a consonant instead
					name += consonants.charAt( rn % consonants.length() );
				}
			} 
			
			// Elsewise, we try to add a consonant
			else {
				if(name.length() == 0) {
					// Add a consonant by default
					name += consonants.charAt( rn % consonants.length() );
				}
				else if(vowels.indexOf( name.charAt(name.length() - 1) ) >= 0) {
					// Add any consonant if the previous is a vowel or softConsonant
					name += consonants.charAt( rn % consonants.length() );
				} else if (name.length() > 1 && softConsonants.indexOf( name.charAt(name.length() - 1) ) >= 0) {
					// Add a hard consonant if the previous was a soft one
					// Don't do it if that soft consonant is the first letter
					name += hardConsonants.charAt( rn % hardConsonants.length() );
				}
				else {
					// Add a vowel instead
					name += vowels.charAt( rn % vowels.length() );
				}	
			}
				
		}
		return name.substring(0,1).toUpperCase() + name.substring(1);
	}
	
	/**
	 * Systematically recolor a given image. Accepts only MAC addresses with length 12. If 
	 * we are provided a hash which is not 16 in length, we will either truncate or prepend
	 * zeroes to the hash and use it.
	 * @param source
	 * @param hash
	 * @return
	 */
	public static Image customizeMinglet(Image source, String hash) {
		if(source == null) return null;
		
		hash = Utils.sanitizeMac(hash);
		
		int base = Utils.getRGB(hash, 0);
		int accent = Utils.getRGB(hash, 3);
		
		// Blue to a base color (body for the minglet)
		source = changeColor(source, 0x0000FF, base);
		// Red to an accent color (wing for the minglet)
		source = changeColor(source, 0xFF0000, accent);
		return source;
	}
	
	/**
	 * Given a hash, generate an RGB integer with an offset.
	 * @param hash
	 * @param i
	 * @return
	 */
	private static int getRGB(String hash, int offset) {
		if(hash.length() < offset + 3) return 0;
		
		String s = new String();
		int l = hash.length() - 1;
		for(int i = 0; i < 3; i++) {
			s += String.valueOf(hash.charAt(2 + offset + i)) + String.valueOf(hash.charAt(l - offset - i));
			//System.out.println("Added indices: " + (offset + i) + ", " + (l - offset - i));
		}
		
		// So the minglets aren't so dark, let's let them be a minimum of
		// 25% bright. This is just so you can see the facial features and
		// everything... I'm not being racist or anything...
		return 0x7F7F7F + ((int) Utils.hexToDecimal( s )) / 2;
	}

	/**
	 * Iterate through an image object and replace the colors in it
	 * @param source
	 * @param fromColor
	 * @param toColor
	 * @return the recolored Image
	 */
	public static Image changeColor(Image source, int fromColor, int toColor) {
		int imageWidth = source.getWidth();
		int imageHeight = source.getHeight();
		
		int[] rgb = new int[imageWidth * imageHeight];
		
		int deviceColor = getDeviceColor(fromColor);
		toColor = getDeviceColor(toColor);
		
		source.getRGB(rgb, 0, imageWidth, 0, 0, imageWidth, imageHeight);
 
		int count = 0;
		for(int i = 0; i < rgb.length; i++)
		{
			if(rgb[i] == deviceColor)
			{
				rgb[i] = toColor;
				count++;
			}
		}
		return Image.createRGBImage(rgb, imageWidth, imageHeight, true);
	}
	
	/**
	 * Change the alpha of a provided Image
	 * @param source
	 * @param alpha
	 * @return the re-alpha'd image
	 */
	public static Image blend(Image source, int alpha) {
		if(alpha > 0xFF || alpha < 0x00) return source;
		alpha = alpha << 24;
		
		int width = source.getWidth();
		int height = source.getHeight();
		
		int [] raw = new int [ width * height ];
		source.getRGB(raw, 0, width, 0, 0, width, height);
		
		for(int i = 0; i < raw.length; i++) {
			// Extract just the color
			//System.out.println("Before: " + Integer.toHexString(raw[i]));
			int color = (raw[i] & 0x00FFFFFF);
			//System.out.println("After: " + Integer.toHexString(color));
			
			// Add in the new alpha
			color += alpha;
			raw[i] = color;
		}
		
		return Image.createRGBImage(raw, width, height, true);
	}
	
	/**
	 * Detects if an image is just purely white
	 * @param source
	 * @return true if it is just a white image, false if not
	 */
	public static boolean isWhite(Image source) {
		int width = source.getWidth();
		int height = source.getHeight();
		
		int [] raw = new int [ width * height ];
		source.getRGB(raw, 0, width, 0, 0, width, height);
		
		for(int i = 0; i < raw.length; i++) {
			int color = (raw[i] & 0x00FFFFFF);
			if(color != 0xFFFFFF) return false;
		}
		return true;
	}
	
	/**
	 * Device colors often do not correspond to "perfect" colors. This
	 * performs appropriate substitutions for getting a device color;
	 * @param color
	 * @return
	 */
	public static int getDeviceColor(int color) {
		Image fake = Image.createRGBImage(new int[]{color}, 1, 1, false);
		
		int[] rgb = new int[1];
		
		fake.getRGB(rgb, 0, 1, 0, 0, 1, 1);
		
		return rgb[0];
	}

	/**
	 * Make a minglet that has never been seen before
	 * @param hash
	 * @param owner
	 * @return a Minglet
	 */
	public static Minglet makeMinglet(String hash, String owner) {
		return makeMinglet(hash, owner, -1);
	}
	
	/**
	 * Make a minglet that has been seen before at the provided time.
	 * @param hash
	 * @param owner
	 * @param time
	 * @return a Minglet
	 */
	public static Minglet makeMinglet(String hash, String owner, long time) {
		Minglet a = new Minglet(hash, owner, time);
		a.addAnimation(Actor.WALK_RIGHT, Actor.KEY_WALK_RIGHT, "/res/animations/minglet/jump-right.png", 34, 34);
		a.addAnimation(Actor.WALK_UP, Actor.KEY_WALK_UP, "/res/animations/minglet/jump-up.png", 34, 34);
		a.addAnimation(Actor.WALK_DOWN, Actor.KEY_WALK_DOWN, "/res/animations/minglet/jump-down.png", 34, 34);
		a.addAnimation(Actor.STAND_STILL, Actor.KEY_STAND, "/res/animations/minglet/standing.png", 28, 29);
		a.addAnimation(Minglet.FALLING, Minglet.KEY_FALL, "/res/animations/minglet/falling-down.png", 38, 38);
		a.addAnimation(Minglet.SMILING, Minglet.KEY_SMILE, "/res/animations/minglet/smiling.png", 28, 29);
		return a;
	}

	/**
	 * Returns a random number using a hex string as a seed
	 * @param id
	 * @param size
	 * @return
	 */
	public static int randWithSeed(String salt, String id, int size) {
		return randWithSeed(Utils.hexToDecimal(salt), Utils.hexToDecimal(id), size);
	}
	
	/**
	 * Get a random number using a hex string as a seed
	 * @param seed, the seed to use
	 * @param size, the upper bound on the random integer
	 * @return random integer
	 */
	public static int randWithSeed(long salt, long seed, int size) {
		return Math.abs( (int) (salt ^ seed) % size );
	}
	
	/**
	 * Workaround to round a double to two decimal places.
	 * @param bmi, the thing to round
	 * @return the rounded number
	 */
	public static double roundDouble(double bmi) {
		return (double)(int)((bmi+0.005)*100.0)/100.0;
	}
	
	/**
	 * Load a file into a vector of strings representing the lines of the file.
	 * @param filename
	 * @return a vector with the file contents
	 */
	public static Vector getLines(String filename) {
		Vector lines = new Vector();
		
		try {
			InputStream is = (new Object()).getClass().getResourceAsStream(filename);
			StringBuffer buffer = new StringBuffer();
			int ch;
			
			while((ch = is.read()) != -1) {
					if(ch != '\n') {
						buffer.append((char) ch);
					}
					else {
						lines.addElement(buffer.toString());
						buffer = new StringBuffer();
					}
			}
			
			if(buffer.length() > 0) {
				lines.addElement(buffer.toString());
			}
		}
		catch(IOException e) {
			System.err.println(e);
		}
		
		return lines;
	}
	
	public static String join(String s[], char delimiter) {
		if(s.length == 0) return "";
		StringBuffer buffer = new StringBuffer();
		buffer.append(s[0]);
		for(int i = 1; i < s.length; i++) {
			buffer.append(delimiter);
			buffer.append(s[i]);
		}
		return buffer.toString();
	}
	
	public static String [] split(String s, char delimiter) {
		String buffer = new String();
		
		boolean flag = false;
		int count = 0;
		
		// Pass 1: Count the number of contiguous blocks of delimiters
		for(int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if(!flag && c != delimiter) {
				count++;
				flag = true;
			}
			else if(flag && c == delimiter) {
				flag = false;
			}
		}
		
		String [] tokens = new String[count];
		flag = false;
		count = 0;
		
		// Pass 2: Populate the array
		for(int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if(!flag && c != delimiter) {
				buffer += c;
				flag = true;
			}
			else if(flag && c != delimiter) {
				buffer += String.valueOf(c);
			}
			else if(flag && c == delimiter) {
				tokens[count] = buffer;
				buffer = "";
				count++;
				flag = false;
			}
		}
		if(buffer.length() > 0 && count == tokens.length - 1) {
			tokens[count] = buffer;
		}
		
		return tokens;
	}
	
	public static String replaceAll(String source, String pattern, String replace) {
		return Utils.replaceAll(source, pattern, replace, false);
	}
	
	public static String replaceAllIgnoreCase(String source, String pattern, String replace) {
		return Utils.replaceAll(source, pattern, replace, true);
	}
	
	private static String replaceAll(String source, String pattern, String replace, boolean ignoreCase) {
		if(pattern.length() > source.length()) return source;
		if(match(pattern,source,ignoreCase)) return replace;
		
		String buffer = new String(source);
		
		for(int i = 0; i < buffer.length() && i + pattern.length() <= buffer.length(); i++) {
			String sub = source.substring(i, i + pattern.length());
			if(match(pattern,sub,ignoreCase)) {
				buffer = buffer.substring(0, i) + replace + buffer.substring(i + pattern.length(), buffer.length() - 1);
			}
		}
		return buffer;
	}
	
	private static boolean match(String a, String b, boolean ignoreCase) {
		return a.compareTo(b)  == 0
			|| (a.toLowerCase().compareTo(b.toLowerCase()) == 0 && ignoreCase); 
	}
	
	private static String sanitizeMac(String in) {
		if(in.length() > 12) {
			in = in.substring(0, 16);
		}
		
		while(in.length() < 12) {
			in = "0" + in;
		}
		return in;
	}

}
